package biolabs.model.motion

import biolabs.core.Genomic
import biolabs.model.CumulationStateful
import biolabs.model.abscretion.Abscretor
import biolabs.space.ContinuousPosition
import biolabs.space.DiscretePosition
import biolabs.space.GridShapedObject

import scala.collection.JavaConversions._

object ChemotaxicMovable {
	trait Genome {
		val chemotaxisSpeeds: Map[Any, Iterable[(Any, Float)]]
	}
}

trait ChemotaxicMovable[
	Self <: ChemotaxicMovable[Self, P, C]
			with CumulationStateful[Self, P]
			with Abscretor[Self, P],
	P <: DiscretePosition[P, C],
	C <: ContinuousPosition[C, P]
]
extends Movable[Self, P]
{

self:
	Self
	with Genomic[ChemotaxicMovable.Genome]
	with GridShapedObject[P, C]
	with Abscretor[Self, P]
=>

	override def motionDelegate(): Option[MotionDelegate[P]] = {
		computeMotion() match {
			case Some(mov) =>
				biolabs.core.Application.LOGGER.fine("desired move " + mov)
				val rmov = mov.rounded()
				if(rmov.isEqualTo(rmov.zero()))
					None
				else
					Some(new MotionDelegate(rmov))
			case None =>
				None
		}
	}

	/**
	 * Computes the desired motion vector based on the current cumulation
	 * state, zygotic graph and gradients of caught substances.
	 */
	private def computeMotion(): Option[C] = {
		val speeds = genome.chemotaxisSpeeds.get(cumulationState) match {
			case Some(speeds) => speeds
			case None => Seq()
		}
		if(speeds.isEmpty)
			return None
		return Some(speeds map {
				case (sub, speed) => getMaxGradient(sub).times(speed)
			} reduceLeft (
				(m1, m2) => m1.plus(m2)
			)
		)
	}

	/**
	 * Returns the normalized gradient of {@code substance}
	 * according to the caught amounts on different receptors.
	 */
	private def computeGradient(substance: Any): C = {
		val c = getShape().getCenterOfGravity()
		getAbscretionPointsFor(substance) .map {
			case (pos, ap) => pos.continuous().minus(c).normalized().times(ap.getSensedAmount())
		} .foldLeft(c.zero()) (
			(a, b) => a.plus(b)
		) normalized()
	}

	private def getMaxGradient(substance: Any): C = {
		val c = getShape().getCenterOfGravity()
		getAbscretionPointsFor(substance) .map {
			case (pos, ap) => (pos.continuous().minus(c), ap.getSensedAmount())
		} .foldLeft((c.zero(), 0.0)) (
			(a, b) => if(b._2 > a._2) b else a
		)._1.normalized()
	}
}